package app_kvEcs;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import logger.LogSetup;

import org.apache.log4j.Level;

import common.messages.KVMessage;
import utils.ClientConnectionUtil;
import app_kvClient.Constants;

/**
 * @author Cipri_L
 *
 */
public class ECS {

	private LogSetup configLogger; // client log
	private MemorySpace servers;
	private ArrayList<ServerNode> serversPool ; 
	private ClientConnectionUtil connectionUtil; // Connection object
	private boolean initialized;
	
	public ECS(){
		this.setupLogger();
		servers = new MemorySpace();
		serversPool = new ArrayList<ServerNode>();
		readConfigFile("server.config");
		initialized = false;
	}
	
	private static void display(String message){
		System.out.println(message);
	}		

	/**
	 * For testing purposes only!
	 * @return
	 */
	public MemorySpace getServers(){
		return servers;
	}
	
	/**
	 * Launch KVServer process at the given number of nodes by SSH calls and initialize them with meta-data.
	 * @param nodes
	 * @throws Exception 
	 */
	public void initService(int nodes, boolean connectionEnabled) {
		if(initialized)
			return;
		initialized = true;
		
		int poolSize = serversPool.size();
		//add n servers to memory space
		for(int i=0; i<nodes ; i++){
			if(i >= poolSize)
				continue;
			ServerNode node = serversPool.get(i);
			if(!connectionEnabled)
				node.server().disableConnection();
			String rellocationMessage = servers.addNode(node);
			display(rellocationMessage);
			display("" + node + "\n"+servers.toString() +"\n");
			//step1:SSH call to run the jar
			//sshCall(node);
		}
	
		display("\n"+servers.toString());
		
		if(connectionEnabled)
			initMetadata();
		
		String message = ">>initService with "+ nodes +" nodes ..";
		display(message);
	}
	
	/** step1:SSH call to run the jar
	 * @param node
	 */
	private void sshCall(ServerNode node){
		//TODO: command: "nohup", "ssh", "-n", host, "java -jar", "ms3-server.jar", port, "&"
		String cd = System.getProperty("user.dir")+ File.separator;
		String command = "java -jar " + cd+"ms3-server.jar " + node.port();
		List<String> params = java.util.Arrays.asList( "java -jar", "ms3-server.jar", node.port());
		
		try {
			Runtime.getRuntime().exec(command);
		} catch (IOException e) {
			e.printStackTrace();
			configLogger.errorLog("ERROR_SSH", e);
		}
	}
	
	/**
	 * step2: initialize servers with metadata
	 */
	private void initMetadata(){
		ServerNode node = servers.firstNode();
		String metadata = servers.getMetadata();
		while(node!=null){			
			initMetadata(node, metadata);
			node = node.next();
		}
	}
	
	private boolean initMetadata(ServerNode node, String metadata){
		try {
			KVMessage response = node.server().connect();
			configLogger.infoLog("connected: "+node, null);
			response = node.server().initMetadata(metadata);
			display("online: "+ node+ " "+ response);
		} catch (Exception e) {
			e.printStackTrace();
			configLogger.errorLog(node.toString(), e);
			return false;
		}
		return true;
	}
	
	/**
	 * start storage service for serving client requests
	 */
	public void start(){
		String message = ">>start server routine";
		display(message);
		ServerNode node = servers.firstNode();
		while(node!=null){			
			start(node);
			node = node.next();
		}
	}

	private boolean start(ServerNode node){
		try {
			KVMessage response = node.server().start();
			configLogger.infoLog("started: "+node.toString() + " "+response, null);
			display("started: "+ node.toString() + " "+ response);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			configLogger.errorLog(node.toString(), e);
		}
		return false;
	}
	
	/**
	 * stop storage service; remote processes remain alive
	 */
	public void stop(){
		String message = ">>stop server routine";
		display(message);
		ServerNode node = servers.firstNode();
		while(node!=null){			
			stopServer(node);
			node = node.next();
		}
	}
	
	private boolean stopServer(ServerNode node){
		try {
			KVMessage response = node.server().stop();
			configLogger.infoLog("stopped: "+ response +" "+node.toString(), null);
			display("stopped: "+ response +" "+ node.toString());
		} catch (Exception e) {
			e.printStackTrace();
			configLogger.errorLog(node.toString(), e);
			return false;
		}
		return true;
	}
	
	/**
	 * exit remote processes 
	 */
	public void shutDown(){
		String message = ">>shutdown server routine";
		display(message);
		ServerNode node = servers.firstNode();
		while(node!=null){			
			try {
				KVMessage response = node.server().shutDown();
				node.server().disconnect();
				configLogger.infoLog("shutDown: "+ response +" "+node.toString(), null);
				display("shutDown: "+ response +" "+ node.toString());
			} catch (Exception e) {
				e.printStackTrace();
				configLogger.errorLog(node.toString(), e);
			}
			node = node.next();
		}
		
		servers.clear();
	}
	
	/**
	 * add a new storage node at arbitrary position
	 */
	public void addNode(){
		String message = ">>add node routine";
		display(message);				
		
		//select node to add - TODO: is not random!!
		ServerNode toAdd = null;
		for(ServerNode node : serversPool){
			if (!node.isAllocated()){
				toAdd = node;
				//try to connect
				boolean connected = initMetadata(toAdd, "");
				if(!connected)
					continue;
				else
					break;
			}				
		}
		
		if(toAdd == null){
			String error = "Server pool empty! No new nodes available!";
			display(error);
			configLogger.infoLog(error, null);	
			return;
		}		
		
		String rellocationMessage = servers.addNode(toAdd);
		try {
			rellocateData(rellocationMessage);
			String msg = "RELOCATE: "+ rellocationMessage;
			display(msg);
			configLogger.infoLog(msg, null);
		} catch (Exception e1) {
			e1.printStackTrace();
			configLogger.errorLog("RELOCATE: "+rellocationMessage, e1);
		}
		
	}
	
	private void updateMetadata(ServerNode node, String metadata){
		try {
			KVMessage response = node.server().updateMetadata(metadata);
			String msg = "Update Metadata: " + response + " "+ node;
			display(msg);
			configLogger.infoLog(msg, null);
		} catch (Exception e) {
			e.printStackTrace();
			configLogger.errorLog("Update Metadata: " + node, e);
		}
	}
	
	private void rellocateData(String rellocationMessage) throws Exception{
		String[] msg = rellocationMessage.split("#");
		for(String m : msg){
			String[] data = m.split(":");
			if(data.length != 6)
				continue;
			String sourceIp = data[0];
			String sourcePort = data[1];
			String startRange = data[2];
			String endRange = data[3];
			String destinationIp = data[4];
			String destinationPort = data[5];
			ServerNode sourceNode = null ;
			ServerNode destinationNode = null;
			for(ServerNode node : serversPool){
				if (!node.isAllocated()){
					continue;
				}
				if(node.ip().equals(sourceIp) && node.port().equals(sourcePort)){
					sourceNode = node;
				} else if (node.ip().equals(destinationIp) && node.port().equals(destinationPort)){
					destinationNode = node;
				}
			}
			if(sourceNode == null || destinationNode == null)
				continue;
			//TODO: protocol for rellocation
			lockWrite(sourceNode);
			lockWrite(destinationNode);
			KVMessage response = sourceNode.server().moveData(destinationIp+":"+destinationPort, startRange, endRange);
			String message = "rellocation: "+ response +" "+ m;
			display(message);
			configLogger.infoLog(message, null);
			updateRangeAll();
			unLockWrite(sourceNode);
			unLockWrite(sourceNode);
		}
	}
	
	private void lockWrite(ServerNode node){
		try {
			KVMessage response = node.server().lockWrite();
			String msg = "LOCK_WRITE: "+ response + " "+ node;
			display(msg);
			configLogger.infoLog(msg, null);
		} catch (Exception e) {
			e.printStackTrace();
			configLogger.errorLog("LOCK_WRITE " + node, e);
		}
	}
	
	private void unLockWrite(ServerNode node){
		try {
			KVMessage response = node.server().unLockWrite();
			String msg = "UNLOCK_WRITE: "+ response + " "+ node;
			display(msg);
			configLogger.infoLog(msg, null);
		} catch (Exception e) {
			e.printStackTrace();
			configLogger.errorLog("UNLOCK_WRITE " + node, e);
		}
	}
	
	/**
	 * remove storage node at arbitrary position
	 */
	public void removeNode(){
		String message = ">>remove node routine";
		display(message);
		
		//select node to add - TODO: is not random!!
		ServerNode toRemove = null;
		for(ServerNode node : serversPool){
			if (node.isAllocated()){
				toRemove = node;
				stopServer(node);
				break;
			}				
		}
		
		if(toRemove == null){
			String error = "Server pool full! No nodes allocated!";
			display(error);
			configLogger.infoLog(error, null);	
			return;
		}	
		
		String rellocationMessage = servers.removeNode(toRemove);
		try {
			rellocateData(rellocationMessage);
			String msg = "RELOCATE: "+ rellocationMessage;
			display(msg);
			configLogger.infoLog(msg, null);
		} catch (Exception e1) {
			e1.printStackTrace();
			configLogger.errorLog("RELOCATE: "+rellocationMessage, e1);
		}
	}
	
	private void updateRangeAll(){
		for(ServerNode node : serversPool){
			if (!node.isAllocated()){
				continue;
			}			
			if(node.isModified())
				try {
					KVMessage response = node.updateRange();
					String msg = "UPDATE_RANGE: "+ response +" "+ node;
					configLogger.infoLog(msg, null);
				} catch (Exception e) {
					e.printStackTrace();
					String error = "ERROR updating metadata! "+ node;
					configLogger.errorLog(error, e);
				}
		}
	}		
	
	private void readConfigFile(String file){
       //load a properties file
		List<String> lines = new LinkedList<String>();
		String message = "Loading config file: "+ file;
		System.out.println(message);
		configLogger.infoLog(message, null);
		try {
			BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
			String line = bufferedReader.readLine();
			
			while (line != null) {
				if(line.contains(":"))
					lines.add(line);
				line = bufferedReader.readLine();
			}
			bufferedReader.close();
		} catch (Exception e) {
			String erroMsg = "ERROR reading configuration file!";
			configLogger.errorLog(erroMsg, e);
			return;
		}
 
		for(String data : lines){
			processServerConfiguration(data);
		}
		
		String result = "";
		result = servers.toString();
		System.out.println(result);
		
	}
	
	private void processServerConfiguration(String data){
		String[] tokens = data.split(":");
		if(tokens.length < 3)
			return;
		String name = tokens[0];
		String ip = tokens[1];
		String port = tokens[2];
		
		ServerNode server = new ServerNode(name, ip, port);
		String message = "Config read: "+ server.toString();
		configLogger.infoLog(message, null);
		System.out.println(message);
		
		serversPool.add(server);
	}
	
		
	private void setupLogger() {
		try {
			configLogger = new LogSetup(Constants.LOGGERS.CONFIG_LOGGER,Level.ALL);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	
	
	
	
	public static void main ( String[] args )
    {
		String file = "";
		if(args.length < 1){
			String message = "No Configuration file! Loading default config ...";
			System.out.println(message);
			file = "server.config";
		} else {
			file = args[0];
		}
		
		ECS ecs = new ECS();
		ecs.initService(10, true);
    }
	
}
